package org.jpropelleralt.ledger.list.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.jpropelleralt.box.BoxFeatures;
import org.jpropelleralt.box.Boxes;
import org.jpropelleralt.box.impl.BoxFeaturesDefault;
import org.jpropelleralt.ledger.list.RecordTransformer;
import org.jpropelleralt.multiverse.Multiverse;
import org.jpropelleralt.node.Node;
import org.jpropelleralt.ref.Ref;
import org.jpropelleralt.universe.Universe;

/**
 * A simple {@link RecordTransformer} that provides fields
 * from {@link Ref}s in the {@link Node} on each line. Can only
 * display {@link Ref}s that have a non-null value class, which is
 * used to provide the column class and to make it type-safe to
 * edit the {@link Ref}s.
 * @param <T>	The type of element
 */
public class RecordTransformerNode<T extends Node> implements RecordTransformer<T> {

	//private final List<Transformer<? super T, ? extends Ref<?>>> transformers;
	private final List<String> names;
	private final List<String> columnNames;
	private final List<Class<?>> classes;
	private final BoxFeatures features;
	private final boolean editable;

	/**
	 * Create a new {@link RecordTransformerNode}
	 * @param <T>			The type of value transformed
	 * @param universe		The {@link Universe} this will inhabit
	 * @param editable		True to allow editing all fields, false for read-only view
	 * @param example		An example of the type of value displayed, used to check Refs are present, and have value classes
	 * @param names			The {@link Ref} names to display, in the order required, or null to display all suitable {@link Ref}s in example.
	 * @return				A new {@link RecordTransformerNode}
	 */
	public static <T extends Node> RecordTransformerNode<T> create(Universe universe, boolean editable, T example, String... names) {
		return new RecordTransformerNode<T>(universe, editable, example, Arrays.asList(names));
	}
	
	/**
	 * Create a new {@link RecordTransformerNode}
	 * @param <T>			The type of value transformed
	 * @param universe		The {@link Universe} this will inhabit
	 * @param editable		True to allow editing all fields, false for read-only view
	 * @param example		An example of the type of value displayed, used to check Refs are present, and have value classes
	 * @param names			The {@link Ref} names to display, in the order required, or null to display all suitable {@link Ref}s in example.
	 * @return				A new {@link RecordTransformerNode}
	 */
	public static <T extends Node> RecordTransformerNode<T> create(Universe universe, boolean editable, T example, List<String> names) {
		return new RecordTransformerNode<T>(universe, editable, example, names);
	}

	/**
	 * Create a new {@link RecordTransformerNode}, allowing editing
	 * @param <T>			The type of value transformed
	 * @param universe		The {@link Universe} this will inhabit
	 * @param example		An example of the type of value displayed, used to check Refs are present, and have value classes
	 * @param names			The {@link Ref} names to display, in the order required, or null to display all suitable {@link Ref}s in example.
	 * @return				A new {@link RecordTransformerNode}
	 */
	public static <T extends Node> RecordTransformerNode<T> create(Universe universe, T example, String... names) {
		return new RecordTransformerNode<T>(universe, true, example, Arrays.asList(names));
	}
	
	/**
	 * Create a new {@link RecordTransformerNode}, allowing editing
	 * @param <T>			The type of value transformed
	 * @param universe		The {@link Universe} this will inhabit
	 * @param example		An example of the type of value displayed, used to check Refs are present, and have value classes
	 * @param names			The {@link Ref} names to display, in the order required, or null to display all suitable {@link Ref}s in example.
	 * @return				A new {@link RecordTransformerNode}
	 */
	public static <T extends Node> RecordTransformerNode<T> create(Universe universe, T example, List<String> names) {
		return new RecordTransformerNode<T>(universe, true, example, names);
	}

	/**
	 * Create a new {@link RecordTransformerNode}
	 * @param <T>			The type of value transformed
	 * @param editable		True to allow editing all fields, false for read-only view
	 * @param example		An example of the type of value displayed, used to check Refs are present, and have value classes
	 * @param names			The {@link Ref} names to display, in the order required, or null to display all suitable {@link Ref}s in example.
	 * @return				A new {@link RecordTransformerNode}
	 */
	public static <T extends Node> RecordTransformerNode<T> create(boolean editable, T example, String... names) {
		return new RecordTransformerNode<T>(Multiverse.getCurrentUniverse(), editable, example, Arrays.asList(names));
	}
	
	/**
	 * Create a new {@link RecordTransformerNode}
	 * @param <T>			The type of value transformed
	 * @param editable		True to allow editing all fields, false for read-only view
	 * @param example		An example of the type of value displayed, used to check Refs are present, and have value classes
	 * @param names			The {@link Ref} names to display, in the order required, or null to display all suitable {@link Ref}s in example.
	 * @return				A new {@link RecordTransformerNode}
	 */
	public static <T extends Node> RecordTransformerNode<T> create(boolean editable, T example, List<String> names) {
		return new RecordTransformerNode<T>(Multiverse.getCurrentUniverse(), editable, example, names);
	}

	/**
	 * Create a new {@link RecordTransformerNode}, allowing editing
	 * @param <T>			The type of value transformed
	 * @param example		An example of the type of value displayed, used to check Refs are present, and have value classes
	 * @param names			The {@link Ref} names to display, in the order required, or null to display all suitable {@link Ref}s in example.
	 * @return				A new {@link RecordTransformerNode}
	 */
	public static <T extends Node> RecordTransformerNode<T> create(T example, String... names) {
		return new RecordTransformerNode<T>(Multiverse.getCurrentUniverse(), true, example, Arrays.asList(names));
	}
	
	/**
	 * Create a new {@link RecordTransformerNode}, allowing editing
	 * @param <T>			The type of value transformed
	 * @param example		An example of the type of value displayed, used to check Refs are present, and have value classes
	 * @param names			The {@link Ref} names to display, in the order required, or null to display all suitable {@link Ref}s in example.
	 * @return				A new {@link RecordTransformerNode}
	 */
	public static <T extends Node> RecordTransformerNode<T> create(T example, List<String> names) {
		return new RecordTransformerNode<T>(Multiverse.getCurrentUniverse(), true, example, names);
	}
	
	/**
	 * Create a new {@link RecordTransformerNode}, allowing editing
	 * @param <T>			The type of value transformed
	 * @param example		An example of the type of value displayed, used to check Refs are present, and have value classes
	 * @param names			The {@link Ref} names to display, in the order required, or null to display all suitable {@link Ref}s in example.
	 * @return				A new {@link RecordTransformerNode}
	 */
	public static <T extends Node> RecordTransformerNode<T> create(T example) {
		return new RecordTransformerNode<T>(Multiverse.getCurrentUniverse(), true, example, (List<String>)null);
	}
	
	private RecordTransformerNode(Universe universe, boolean editable, T example, String... names) {
		this(universe, editable, example, Arrays.asList(names));
	}
	
	private RecordTransformerNode(Universe universe, boolean editable, T example, List<String> names) {
		super();
		this.features = new BoxFeaturesDefault(this, universe);
		this.editable = editable;

		//Make a list of all ref names in the example that 
		//point to a Ref with a value class.
		List<String> exampleNames = new ArrayList<String>();
		for (String refName : example) {
			if (example.get(refName).valueClass()!=null) {
				exampleNames.add(refName);
			}
		}
		
		List<String> namesToUse;
		//If no names are specified, just use all names from example Node
		if (names == null) {
			namesToUse = exampleNames;
			
		//Otherwise, use all specified names that are actually found in the example Node
		} else {
			namesToUse = new ArrayList<String>(names);
			namesToUse.retainAll(exampleNames);
		}
		
		this.names = Collections.unmodifiableList(namesToUse);
		
		//Now make list of classes and column names
		Class<?> exampleClass = example.getClass();
		classes = new ArrayList<Class<?>>(this.names.size());
		columnNames = new ArrayList<String>(this.names.size());
		for (String name : this.names) {
			classes.add(example.get(name).valueClass());
			columnNames.add(Boxes.localisedName(exampleClass, name));
		}
	}

	@Override
	public boolean editable(int record, T recordObject, int field) {
		return editable;
	}

	@Override
	public Class<?> fieldClass(int field) {
		return classes.get(field);
	}

	@Override
	public int fieldCount() {
		return names.size();
	}

	@Override
	public String fieldName(int field) {
		return columnNames.get(field);
	}

	@Override
	public Object get(int record, T recordObject, int field) {
		return recordObject.get(names.get(field)).get();
	}

	//Types are slightly complicated here...
	//Firstly, we check that the ref we want to set is not null,
	//and has a valueClass - if not we don't try to set.
	//Secondly, we now know that we can set the Ref to either null,
	//or something that is assignable to the ref valueClass - so
	//after checking this we can use the Ref as a raw type to allow
	//us to set the value. This relies on Ref's guarantee that the
	//valueClass it returns (if not null) is the exact same type as the
	//parametric value type.
	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Override
	public void set(int record, T recordObject, int field, Object value) {
		Ref<?> ref = recordObject.get(names.get(field));
		if (ref == null || ref.valueClass() == null) {
			return;
		}
		if (value == null || ref.valueClass().isAssignableFrom(value.getClass())) {
			((Ref)ref).set(value);
		}
	}

	@Override
	public BoxFeatures features() {
		return features;
	}

}
